// 
//  avmap.vala
//  
//  Author:
//       Alexandre Rosenfeld <alexandre.rosenfeld@gmail.com>
//  
//  Copyright (c) 2011 Alexandre Rosenfeld
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Lesser General Public License for more details.
// 
//  You should have received a copy of the GNU Lesser General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.

namespace mShare.AirVideo {
	
	public errordomain Serialize.Error {
		UNSUPPORTED_VALUE,
		UNKNOWN_CODE
	}
	
	public interface ISerializable : Object {
		public abstract HashTable<string,Value?> dump();
		public abstract void restore(HashTable<string,Value?> data);
	}
	
	public class SerializableData : Object, ISerializable {
		
		public HashTable<string,Value?> data { get; set; }
		public string klass_name { get; set; }
		
		public SerializableData() {
			this.data = new HashTable<string,Value?>(str_hash, str_equal);
		}
		public HashTable<string,Value?> dump() {
			return this.data;
		}
		public void restore(HashTable<string,Value?> data) {
			data.foreach((key, value) => {
				this.data.insert(key, value);
			});
		}
	}
	
	public abstract class SerializableObject : GLib.Object, ISerializable {
		public void restore(HashTable<string,Value?> data) {
			data.foreach((key, value) => {
				if (this.get_class().find_property(key) != null) {
					this.set_property(key, value);
				} else {
					warning("Property %s not found", key);
				}
			});
		}
		
		public HashTable<string,Value?> dump() {
			HashTable<string,Value?> values = new HashTable<string,Value?>(str_hash, str_equal);
			foreach (ParamSpec spec in this.get_class().list_properties()) {
				debug("Found prop %s", spec.name);
				if (spec.name == "klass-name")
					continue;
				
				var name = spec.name;
				if (name.get(name.length - 1) == '-')
					name = name.substring(0, name.length - 1);
				
				Value value = Value(spec.value_type);
				this.get_property(spec.name, ref value);
				
				values.insert(name, value);
			}
			return values;
		}
	}
	
	[CCode (cname = "air_connect_Response")]
	public class Response : SerializableObject {
		public string? errorMessage { get; set; default = null; }
		public string? errorReport { get; set; default = null; }
		public int32 state { get; set; default = 0; }
		public Value result { get; set; }
	}
	
	[CCode (cname = "air_video_PathItem")]
	public class PathItem : SerializableObject {
		public string? itemId { get; set; default = null; }
		public int type_ { get; set; default = 0; }
		public string? name { get; set; default = null; }
	}
	
	public interface Factory {
		public abstract Type? lookup(string object_name, HashTable<string,Value?> data);
	}
	
	public class ObjectFactory {
		public Type? lookup(string object_name, HashTable<string,Value?> data) {
			var t = typeof(SerializableObject);
			foreach (Type c in t.children()) {
				
			}
			return null;
		}
	}
	
	private HashTable<string,Value?> dump_object(Object obj) {
		HashTable<string,Value?> values = new HashTable<string,Value?>(str_hash, str_equal);
		foreach (ParamSpec spec in obj.get_class().list_properties()) {
			debug("Found prop %s", spec.name);
			if (spec.name == "klass-name")
				continue;
			
			var name = spec.name;
			if (name.get(name.length - 1) == '-')
				name = name.substring(0, name.length - 1);
			
			Value value = Value(spec.value_type);
			obj.get_property(spec.name, ref value);
			
			values.insert(name, value);
		}
		return values;
	}
	
	private void restore_object(Object obj, HashTable<string,Value?> data) {
		data.foreach((key, value) => {
			if (obj.get_class().find_property(key) != null) {
				obj.set_property(key, value);
			} else {
				warning("Property %s not found", key);
			}
		});
	}
	
	public void serialize(OutputStream output, Value? data) throws Serialize.Error, GLib.IOError {
		DataOutputStream stream = output as DataOutputStream;
		if (stream == null) {
			stream = new DataOutputStream(output);
			stream.set_close_base_stream(false);
		}
		stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
		
		debug("Got %s to serialize", data.type_name());
		
		if (data == null || (data.holds(typeof(string)) && (string)data == null)) {
			stream.put_byte('n');
		} else if (data.holds(typeof(Value))) {
			serialize(stream, (Value?)data.get_boxed());
		} else if (data.holds(typeof(ValueArray))) {
			unowned ValueArray array = data as ValueArray;
			stream.put_byte('a');
			stream.put_uint32(0);
			stream.put_uint32(array.n_values);
			foreach (Value item in array) {
				serialize(stream, item);
			}
		} else if (data.holds(typeof(Object))) {
			//unowned ISerializable obj = data.get_object() as ISerializable;
			Object obj = data as Object;
			stream.put_byte('o');
			stream.put_uint32(0);
			string name = obj.get_type().name().replace("_", ".");
			debug("Got object (%s)", name);
			stream.put_uint32(name.length);
			stream.write(name.data);
			stream.put_uint32(0); //version
			HashTable<string,Value?> values = dump_object(obj);
			uint32 n_items = values.size();
			stream.put_uint32(n_items);
			debug("Serializing properties (%u items)", n_items);
			foreach (string key in values.get_keys()) {
				//if (key == "name")
				//	continue;
				Value? val = values.lookup(key);
				
				if (val == null)
					debug("null prop");
				debug("Property: %s = %s", key, val.strdup_contents());
				
				stream.put_uint32(key.length);
				stream.write(key.data);
				serialize(stream, val);
			}
			debug("Done with object");
		} else if (data.holds(typeof(string))) {
			string str = (string) data;
			stream.put_byte('s'); 
			stream.put_uint32(0);
			stream.put_uint32(str.length);
			stream.put_string(str);
		} else if (data.holds(typeof(int32))) {
			stream.put_byte('i');
			stream.put_int32((int32)data);
		} else if (data.holds(typeof(int64))) {
			stream.put_byte('l');
			stream.put_int64((int64)data);
		} else {
			throw new Serialize.Error.UNSUPPORTED_VALUE("Value type %s unsupported", data.type_name());
		}
	}
		
	static int ident = 0;
	
	public Value? parse(InputStream input) throws Serialize.Error, GLib.IOError {
		//static int ident = 0;
		
		DataInputStream stream = input as DataInputStream;
		if (stream == null)
			stream = new DataInputStream(input);
		stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN);
		
		uchar id = stream.read_byte();
		
		switch (id) {
			case 'o':
				stream.read_uint32();
				uint32 name_length = stream.read_uint32();
				uint8[] name = new uint8[name_length];
				stream.read(name);
				stream.read_uint32(); //version
				uint32 length = stream.read_uint32();
				//TODO: Use factory and class names for object instantiation
				SerializableData object = new SerializableData();
				//SerializableObject obj = new SerializableObject();
				object.klass_name = (string)name;
				debug ("%s Object (name: %s, items: %u) {", string.nfill(ident, '\t'), object.klass_name, length);
				ident++;
				var values = new HashTable<string,Value?>(str_hash, str_equal);
				for (int i = 0; i < length; i++) {
					uint32 key_length = stream.read_uint32();
					uint8[] key = new uint8[key_length];
					stream.read(key);
					debug ("%s %s:", string.nfill(ident, '\t'), (string) key);
					ident++;
					Value? data = parse(stream);
					debug ("%s %s", string.nfill(ident, '\t'), data.strdup_contents());
					ident--;
					values.insert((string)key, data);
				}
				//object.restore(values);
				restore_object(object, values);
				ident--;
				debug ("%s }", string.nfill(ident, '\t'));
				return object;
			case 'a':
			case 'e':
				stream.read_uint32();
				uint32 length = stream.read_uint32();
				var data = new ValueArray(length);
				debug ("%s Array (items: %u) [", string.nfill(ident, '\t'), length);
				ident++;
				for (int i = 0; i < length; i++)
					data.append(parse(input));
				ident--;
				debug ("%s ]", string.nfill(ident, '\t'));
				return data;
			case 'x':
			case 's':
				stream.read_uint32();
				uint32 length = stream.read_uint32();
				uint8[] data = new uint8[length];
				stream.read(data);
				if (id == 'x') {
					debug("%s <Data, length: %u>", string.nfill(ident, '\t'), length);
					ByteArray bytes = new ByteArray.sized(length);
					bytes.append(data);
					return bytes;
				} else {
					string str = (string) data;
					return str;
				}
			case 'n':
				return null;
			case 'i':
			case 'r':
				return stream.read_uint32();
			case 'l':
				return stream.read_int64();
			case 'd':
				return stream.read_int64();
			default:
				throw new Serialize.Error.UNKNOWN_CODE("Unkown code '%c' found", id);
		}
	}
}